Skip to content

perf(agc): hand pending results over to an image whose DCC keys are a clear code - #7427

Open
parl0v wants to merge 7 commits into
boykopovar:mainfrom
parl0v:perf/clear-keys-hand-over
Open

parl0v wants to merge 7 commits into
boykopovar:mainfrom
parl0v:perf/clear-keys-hand-over

Conversation

@parl0v

@parl0v parl0v commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What

StorageTexture::Refresh stores every other image whose results are still pending in the image's memory before it uploads (FlushPending(..., "storage refresh")). When the refreshed image's DCC keys are a clear code, its upload is a GPU clear (upload, IsDccClear + ClearColorFor) that reads none of that memory, so those stores are dead for it. Unreal's render graph hits this constantly: it hands the memory of a transient texture whose results are still pending to a new texture that it fast-clears.

On Minecraft Dungeons II (PPSA16064) I counted this at the title screen (RTX 4060, 3072 MiB storage cache, per 10 s): 86,776 storage refreshes, 20,298 of them over memory where other images still had results pending, and 44.3 of those 49.2 GB of pending results (90 %) sat under a refreshed image whose keys were a clear code. None of them reached past the refreshed image. The storage refresh write-backs alone were 53.7 GB per 10 s, about 15 ms of GPU time per frame.

  • perf: StorageTexture::clearHandOver (called by Refresh before its flush) leaves those results pending instead of storing them, when:

    • the image has block units (64 KiB) and DCC keys, holds no results of its own, and its memory is write-watched. In compared memory the refresh's compare and the clear's snapshot both read the guest bytes through the flush hook, which stores the results anyway, so nothing changes there;
    • every other pending image over the memory stores unit by unit (no DCC keys, or uncompressed ones), none of its keys overlap this image's keys, and each of its pending units that touches the image lies wholly inside it;
    • ProvedKeys() returns a clear code that ClearColorFor accepts.

    The path is chosen from the keys read before any flush, and only when no flush runs at all, so no store can change the keys between the decision and the upload. Refresh then reuses those keys instead of reading them again.

  • What happens to the results afterwards:

    • Read first: anything that reads the memory before the image is written (the flush hook, a buffer region, another image's refresh) stores them as before.
    • Superseded: once the image's own results cover a unit (markLayersPending), the handed-over results in that unit are dropped, the same way the existing borrow path drops an alias's units: the image's write-back stores every byte of the unit.
    • Written again: an image written since the hand-over keeps its newer results (tracked by a write count; an upload of the source by its own refresh does not count as a write).
    • Stored late: a unit stored after the hand-over (a read came first) stamped the image's memory after its generation. That store came before the image's own write, so at that write the unit's generation moves past it, and the write-back's keep rule does not treat it as a CPU store over the image's results. A block with a CPU store since the image's generation keeps its generation, so the CPU's bytes win there as before. The hand-over state is reset at every refresh.
  • APS5_NO_CLEAR_HAND_OVER=1 stores the results first, as before. With APS5_PROFILE_DRAW the [storage] line counts the hand-overs and the units dropped.

  • test: agc_storage_clear_hand_over (agc_driver_recorder_tests --clear-hand-over-only). An RGBA8 image A has results over four 64 KiB units; an image B of another format and shape lies over the first two, with DCC keys that are a clear code. Four orders run with the hand-over on and off: A never read again, A read before B writes, A written again after the hand-over, and a CPU store into one of A's units under B before B writes. Each must leave the same texels in B and the same guest bytes as the old order. With the hand-over on, B's refresh must store nothing, A must stay pending, and B's write must drop A's two units under B. The test runs in watched memory (its host import kept watched) and in compared memory, where no hand-over may happen. The test layout is close to the one in perf(agc): hand aliased storage memory over without the guest round trip #4286, which tried a broader hand-over and was closed when it showed no gain in a default configuration.

Tested

Windows 10 19045, MinGW GCC, Release. Minecraft Dungeons II (PPSA16064) title screen. FPS is presents per 10 s, averaged over each row's last windows. Rows alternate between main and main + this commit (main b39d90b for these runs), with the same relinked executable.

  • RTX 3060 12 GB (i5-12400F, NVIDIA 617.42), default settings (automatic storage budget), 215 s rows (FPS over the last 100 s), APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1:

    main this PR
    round 1 7.62 FPS, GPU busy 96 ms 8.30 FPS, GPU busy 87 ms
    round 2 7.43 FPS, GPU busy 98 ms 8.50 FPS, GPU busy 87 ms
    mean 7.53 8.40 (+11.6 %)
  • RTX 4060 8 GB (i5-14400F, NVIDIA 617.42), APS5_TEXTURE_CACHE_MIB=3072, 150 s rows (FPS over the last 60 s), APS5_PROFILE_DRAW=1:

    main this PR
    round 1 7.47 FPS, GPU busy 94 ms 8.27 FPS, GPU busy 86 ms
    round 2 7.37 FPS, GPU busy 95 ms 7.90 FPS, GPU busy 86 ms
    mean 7.42 8.09 (+9.0 %)
    storage refresh write-backs per 10 s 12,161 / 53.7 GB 5,289 / 12.7 GB
    hand-overs per 10 s 4,156 (736,195 units dropped)
  • On the RTX 4060 with its automatic budget (1.5-1.8 GB on 8 GB) the frame is bound by storage cache evictions, and this change makes no difference: 3.07 / 3.05 FPS on main against 3.12 / 2.90 with this PR.

  • On this branch, rebased on main 66e71b6, the 61 tests that run agc_driver_recorder_tests, agc_dcc_key_sharing_tests and agc_driver_sampled_cache_budget_tests pass on the RTX 4060. That covers agc_storage_clear_hand_over, every agc_storage_* test, agc_unit_shadow, agc_pending_key_stores, agc_shadowed_key_stores, agc_dcc_key_sharing and agc_driver_resource_cache_*; agc_import_settle_first_use skips on this machine.

  • tools/check_conventions.py --base origin/main passes.

Performance capture (as the Performance report form describes): Minecraft Dungeons II (PPSA16064) title screen, RTX 3060 12 GB (i5-12400F), NVIDIA 617.42, Windows 10.0.19045, 3840x2160 output. Only APS5_TRACE_SECONDS=120:12 set, caches warm (the title ran once on the build and was restarted), stderr not redirected. main 48d61f5 against this PR's head 9371b6b:

Frame and GPU times (ms):

Metric Baseline Candidate Change Change (%)
frame_ms.p50 117.162 104.904 -12.258 -10.462
frame_ms.p90 145.047 144.955 -0.092 -0.063
frame_ms.p99 158.422 150.801 -7.621 -4.811
frame_ms.max 158.422 151.214 -7.208 -4.550
mean_frame_ms 121.758 110.522 -11.235 -9.228
gpu_busy_ms_per_present 100.711 92.270 -8.441 -8.381

Top phases (ms per frame):

Metric Baseline Candidate Change Change (%)
Driver.Dispatch 85.879 77.593 -8.286 -9.648
Driver.Packet 242.204 219.818 -22.386 -9.243
Driver.Packet.device_lock 34.172 33.061 -1.111 -3.251
Driver.Packet.dispatch 86.382 78.110 -8.272 -9.576
Driver.Packet.memory_wait 68.067 60.483 -7.584 -11.142
Driver.Packet.prepare_memory 30.126 25.538 -4.588 -15.229
Vulkan.Dispatch 40.265 35.322 -4.943 -12.276
Vulkan.Dispatch.shader_resources 38.025 32.803 -5.222 -13.733
{"commit":"48d61f53b46f","dirty":false,"env":{"APS5_TRACE_SECONDS":"120:12"},"gpu":"NVIDIA GeForce RTX 3060","driver":"NVIDIA 617.42","os":"Windows 10.0.19045","resolution":"3840x2160","window":{"spec":"seconds 120:12","first_present":2158,"ms":12053.995},"presents":99,"frame_ms":{"p50":117.162,"p90":145.047,"p99":158.422,"max":158.422},"gpu_busy_ms_per_present":100.711,"worker_busy_pct":{"queue worker 0x0":99.421,"queue worker 0x20":99.581},"top_phases_ms_per_frame":{"Driver.Packet":242.204,"Driver.Packet.dispatch":86.382,"Driver.Dispatch":85.879,"Driver.Packet.memory_wait":68.067,"Vulkan.Dispatch":40.265,"Vulkan.Dispatch.shader_resources":38.025,"Driver.Packet.device_lock":34.172,"Driver.Packet.prepare_memory":30.126},"trace_dropped":false,"trace":"aps5-trace-48d61f53b46f-9720.json.gz"}
{"commit":"9371b6b498e8","dirty":false,"env":{"APS5_TRACE_SECONDS":"120:12"},"gpu":"NVIDIA GeForce RTX 3060","driver":"NVIDIA 617.42","os":"Windows 10.0.19045","resolution":"3840x2160","window":{"spec":"seconds 120:12","first_present":2225,"ms":12046.908},"presents":109,"frame_ms":{"p50":104.904,"p90":144.955,"p99":150.801,"max":151.214},"gpu_busy_ms_per_present":92.270,"worker_busy_pct":{"queue worker 0x0":99.355,"queue worker 0x20":99.627},"top_phases_ms_per_frame":{"Driver.Packet":219.818,"Driver.Packet.dispatch":78.110,"Driver.Dispatch":77.593,"Driver.Packet.memory_wait":60.483,"Vulkan.Dispatch":35.322,"Driver.Packet.device_lock":33.061,"Vulkan.Dispatch.shader_resources":32.803,"Driver.Packet.prepare_memory":25.538},"trace_dropped":false,"trace":"aps5-trace-9371b6b498e8-6060.json.gz"}

Traces: aps5-trace-48d61f53b46f-9720.json.gz (main) and aps5-trace-9371b6b498e8-6060.json.gz (this PR), attached below.

trace_digest.py, main

seconds 120:12 from present 2158: 99 presents, frame p50 117.2 / p90 145.0 / p99 158.4 / max 158.4 ms, GPU busy 100.7 ms per present
NVIDIA GeForce RTX 3060, NVIDIA 617.42, Windows 10.0.19045, 3840x2160, commit 48d61f5, dropped: no

{"commit":"48d61f53b46f","dirty":false,"env":{"APS5_TRACE_SECONDS":"120:12"},"gpu":"NVIDIA GeForce RTX 3060","driver":"NVIDIA 617.42","os":"Windows 10.0.19045","resolution":"3840x2160","window":{"spec":"seconds 120:12","first_present":2158,"ms":12053.995},"presents":99,"frame_ms":{"p50":117.162,"p90":145.047,"p99":158.422,"max":158.422},"gpu_busy_ms_per_present":100.711,"worker_busy_pct":{"queue worker 0x0":99.421,"queue worker 0x20":99.581},"top_phases_ms_per_frame":{"Driver.Packet":242.204,"Driver.Packet.dispatch":86.382,"Driver.Dispatch":85.879,"Driver.Packet.memory_wait":68.067,"Vulkan.Dispatch":40.265,"Vulkan.Dispatch.shader_resources":38.025,"Driver.Packet.device_lock":34.172,"Driver.Packet.prepare_memory":30.126},"trace_dropped":false,"trace":"aps5-trace-48d61f53b46f-9720.json.gz"}

Slowest frames (largest slices inside each, inclusive)

  • present 2199: 158.4 ms at 4990 ms: Driver.Packet (queue worker 0x20) 156.8, Driver.Packet (queue worker 0x0) 152.3, batch (gpu) 105.5, Driver.Packet.dispatch (queue worker 0x20) 75.5
  • present 2183: 157.2 ms at 3021 ms: Driver.Packet (queue worker 0x20) 155.6, Driver.Packet (queue worker 0x0) 150.7, batch (gpu) 105.6, Driver.Packet.dispatch (queue worker 0x20) 74.6
  • present 2246: 153.0 ms at 10688 ms: Driver.Packet (queue worker 0x20) 151.5, Driver.Packet (queue worker 0x0) 146.6, batch (gpu) 108.3, Driver.Packet.dispatch (queue worker 0x20) 74.8
  • present 2166: 149.2 ms at 933 ms: Driver.Packet (queue worker 0x20) 147.7, Driver.Packet (queue worker 0x0) 142.9, batch (gpu) 107.0, Driver.Packet.dispatch (queue worker 0x20) 68.2
  • present 2198: 148.9 ms at 4841 ms: Driver.Packet (queue worker 0x20) 147.2, Driver.Packet (queue worker 0x0) 142.6, batch (gpu) 106.4, Driver.Packet.dispatch (queue worker 0x20) 69.5

Threads (ms per frame)

thread busy wait untraced in slices < 20 us largest
queue worker 0x0 85.8 (70 %) 29.6 (24 %) 6.3 34.1 Driver.Packet 115.4, Driver.EndSubmission 0.0
queue worker 0x20 39.7 (33 %) 80.2 (66 %) 1.8 7.3 Driver.Packet 119.8, Driver.EndSubmission 0.1
presenter 0.6 (0 %) 0.0 (0 %) 121.1 0.1 VideoOut.Flip 0.6, Driver.Present 0.0
gpu 100.7 (83 %) 0.0 (0 %) 21.1 0.0 batch 100.7

Largest slices (ms per frame, inclusive, all threads): Driver.Packet 235.3, batch 100.7, Driver.Packet.dispatch 86.1, Driver.Dispatch 85.2, Driver.Packet.memory_wait 68.0, Vulkan.Dispatch 39.4, Vulkan.Dispatch.shader_resources 37.1, Driver.Packet.device_lock 32.2, Driver.Packet.prepare_memory 27.8, Recorder.Sync 20.7

Longest waits

  • Driver.Packet.memory_wait (queue worker 0x20) 45.5 ms at 10797 ms, present 2246
  • Driver.Packet.memory_wait (queue worker 0x20) 45.1 ms at 5104 ms, present 2199
  • Driver.Packet.memory_wait (queue worker 0x20) 44.3 ms at 1038 ms, present 2166
  • Driver.Packet.memory_wait (queue worker 0x20) 43.9 ms at 8841 ms, present 2230
  • Driver.Packet.memory_wait (queue worker 0x20) 43.0 ms at 3137 ms, present 2183
  • Driver.Packet.memory_wait (queue worker 0x20) 41.8 ms at 6905 ms, present 2214
  • Driver.Packet.memory_wait (queue worker 0x20) 41.5 ms at 4949 ms, present 2198
  • Driver.Packet.memory_wait (queue worker 0x20) 41.1 ms at 8989 ms, present 2231
  • Driver.Packet.memory_wait (queue worker 0x20) 41.1 ms at 2981 ms, present 2182
  • Driver.Packet.memory_wait (queue worker 0x20) 40.4 ms at 7050 ms, present 2215

Largest GPU batches (GPU ranges inside, and the CPU slices that recorded each, inclusive)

  • batch 4140: 11.6 ms at 7393 ms, present 2218: GPU no ranges; recorded by Recorder.WaitSerial (queue worker 0x20) 11.7, Driver.Packet (queue worker 0x20) 1.9, Driver.Packet.dispatch (queue worker 0x20) 1.4, Driver.Dispatch (queue worker 0x20) 1.3
  • batch 163: 11.5 ms at 303 ms, present 2160: GPU no ranges; recorded by Driver.Packet (queue worker 0x20) 1.5, Driver.Packet.dispatch (queue worker 0x20) 1.0, Driver.Dispatch (queue worker 0x20) 0.9, Driver.Packet.prepare_memory (queue worker 0x20) 0.5
  • batch 2466: 11.5 ms at 4439 ms, present 2194: GPU no ranges; recorded by Recorder.WaitSerial (queue worker 0x20) 13.9, Driver.Packet (queue worker 0x20) 1.5, Driver.Packet.dispatch (queue worker 0x20) 1.3, Driver.Dispatch (queue worker 0x20) 1.2
  • batch 3269: 11.5 ms at 5830 ms, present 2205: GPU no ranges; recorded by Driver.Packet (queue worker 0x20) 1.8, Driver.Packet.dispatch (queue worker 0x20) 1.2, Driver.Dispatch (queue worker 0x20) 1.1, GpuMutex.wait.label (queue worker 0x20) 0.8
  • batch 988: 11.4 ms at 1761 ms, present 2172: GPU no ranges; recorded by Driver.Packet (queue worker 0x20) 1.6, Driver.Packet.dispatch (queue worker 0x20) 1.1, Driver.Dispatch (queue worker 0x20) 1.1, GpuMutex.wait.label (queue worker 0x20) 0.6
trace_digest.py, this PR

seconds 120:12 from present 2225: 109 presents, frame p50 104.9 / p90 145.0 / p99 150.8 / max 151.2 ms, GPU busy 92.3 ms per present
NVIDIA GeForce RTX 3060, NVIDIA 617.42, Windows 10.0.19045, 3840x2160, commit 9371b6b, dropped: no

{"commit":"9371b6b498e8","dirty":false,"env":{"APS5_TRACE_SECONDS":"120:12"},"gpu":"NVIDIA GeForce RTX 3060","driver":"NVIDIA 617.42","os":"Windows 10.0.19045","resolution":"3840x2160","window":{"spec":"seconds 120:12","first_present":2225,"ms":12046.908},"presents":109,"frame_ms":{"p50":104.904,"p90":144.955,"p99":150.801,"max":151.214},"gpu_busy_ms_per_present":92.27,"worker_busy_pct":{"queue worker 0x0":99.355,"queue worker 0x20":99.627},"top_phases_ms_per_frame":{"Driver.Packet":219.818,"Driver.Packet.dispatch":78.11,"Driver.Dispatch":77.593,"Driver.Packet.memory_wait":60.483,"Vulkan.Dispatch":35.322,"Driver.Packet.device_lock":33.061,"Vulkan.Dispatch.shader_resources":32.803,"Driver.Packet.prepare_memory":25.538},"trace_dropped":false,"trace":"aps5-trace-9371b6b498e8-6060.json.gz"}

Slowest frames (largest slices inside each, inclusive)

  • present 2291: 151.2 ms at 7266 ms: Driver.Packet (queue worker 0x20) 149.5, Driver.Packet (queue worker 0x0) 145.0, batch (gpu) 107.3, Driver.Packet.dispatch (queue worker 0x20) 80.3
  • present 2275: 150.8 ms at 5504 ms: Driver.Packet (queue worker 0x20) 149.1, Driver.Packet (queue worker 0x0) 144.2, batch (gpu) 106.6, Driver.Packet.dispatch (queue worker 0x20) 77.4
  • present 2243: 150.8 ms at 1977 ms: Driver.Packet (queue worker 0x20) 149.2, Driver.Packet (queue worker 0x0) 144.4, batch (gpu) 107.6, Driver.Packet.dispatch (queue worker 0x20) 76.2
  • present 2260: 149.6 ms at 3888 ms: Driver.Packet (queue worker 0x20) 148.1, Driver.Packet (queue worker 0x0) 143.4, batch (gpu) 106.7, Driver.Packet.dispatch (queue worker 0x20) 75.9
  • present 2323: 148.7 ms at 10800 ms: Driver.Packet (queue worker 0x20) 147.0, Driver.Packet (queue worker 0x0) 142.6, batch (gpu) 105.2, Driver.Packet.dispatch (queue worker 0x20) 78.5

Threads (ms per frame)

thread busy wait untraced in slices < 20 us largest
queue worker 0x0 79.9 (72 %) 24.3 (22 %) 6.3 34.1 Driver.Packet 104.2, Driver.EndSubmission 0.0
queue worker 0x20 36.3 (33 %) 72.5 (66 %) 1.7 7.2 Driver.Packet 108.7, Driver.EndSubmission 0.1
presenter 0.6 (1 %) 0.0 (0 %) 109.9 0.1 VideoOut.Flip 0.6, Driver.Present 0.0
gpu 91.4 (83 %) 0.0 (0 %) 19.1 0.0 batch 91.4

Largest slices (ms per frame, inclusive, all threads): Driver.Packet 212.9, batch 91.4, Driver.Packet.dispatch 77.8, Driver.Dispatch 76.9, Driver.Packet.memory_wait 60.4, Vulkan.Dispatch 34.4, Vulkan.Dispatch.shader_resources 31.9, Driver.Packet.device_lock 31.0, Driver.Packet.prepare_memory 23.2, Recorder.WaitSerial 19.9

Longest waits

  • Driver.Packet.memory_wait (queue worker 0x20) 45.1 ms at 2084 ms, present 2243
  • Driver.Packet.memory_wait (queue worker 0x20) 43.6 ms at 5612 ms, present 2275
  • Driver.Packet.memory_wait (queue worker 0x20) 43.4 ms at 3995 ms, present 2260
  • Driver.Packet.memory_wait (queue worker 0x20) 43.1 ms at 462 ms, present 2228
  • Driver.Packet.memory_wait (queue worker 0x20) 42.7 ms at 3846 ms, present 2259
  • Driver.Packet.memory_wait (queue worker 0x20) 42.3 ms at 2233 ms, present 2244
  • Driver.Packet.memory_wait (queue worker 0x20) 42.3 ms at 10907 ms, present 2323
  • Driver.Packet.memory_wait (queue worker 0x20) 42.1 ms at 7376 ms, present 2291
  • Driver.Packet.memory_wait (queue worker 0x20) 41.9 ms at 9288 ms, present 2308
  • Driver.Packet.memory_wait (queue worker 0x20) 41.4 ms at 7522 ms, present 2292

Largest GPU batches (GPU ranges inside, and the CPU slices that recorded each, inclusive)

  • batch 1222: 14.9 ms at 2049 ms, present 2243: GPU no ranges; recorded by Driver.Packet (queue worker 0x20) 1.8, Driver.Packet.dispatch (queue worker 0x20) 1.5, Driver.Dispatch (queue worker 0x20) 1.5, GpuMutex.wait.label (queue worker 0x20) 0.4
  • batch 4282: 13.6 ms at 7219 ms, present 2290: GPU no ranges; recorded by Recorder.WaitSerial (queue worker 0x20) 15.4, Driver.Packet (queue worker 0x20) 1.5, Driver.Packet.dispatch (queue worker 0x20) 1.2, Driver.Dispatch (queue worker 0x20) 1.1
  • batch 6431: 12.1 ms at 10875 ms, present 2323: GPU no ranges; recorded by no CPU slices
  • batch 4041: 11.8 ms at 6808 ms, present 2286: GPU no ranges; recorded by Recorder.WaitSerial (queue worker 0x20) 15.4, Driver.Packet (queue worker 0x20) 1.4, Driver.Packet.dispatch (queue worker 0x20) 1.2, Driver.Dispatch (queue worker 0x20) 1.1
  • batch 853: 11.8 ms at 1424 ms, present 2237: GPU no ranges; recorded by Driver.Packet (queue worker 0x20) 1.6, Driver.Packet.dispatch (queue worker 0x20) 1.5, Driver.Dispatch (queue worker 0x20) 1.4, Vulkan.Dispatch (queue worker 0x20) 0.7

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4060 8 GB, driver 617.42, Windows 10 (tests and title runs); NVIDIA GeForce RTX 3060 12 GB, driver 617.42, Windows 10 (title runs)
  • AI-assisted: yes

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a bounds issue in the new regression fixture: surfaceB is 128 × 128 × 8 = 131,072 bytes, and it starts at address + surfaceA, but bytes (the allocation and registered/imported extent) is only surfaceA + 65,536. That leaves the second half of image B outside the allocated guest block. The readback and final flush still cover the full 131,072-byte image, so the test can access memory beyond the fixture allocation. Could you size/register the block through surfaceA + surfaceB (and keep any intentional overlap explicit), then rerun the focused test?

@parl0v

parl0v commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@MarcZX100 thanks for reading the fixture closely. B doesn't start at address + surfaceA: surface() sets baseAddress = address for both images, and its fourth argument is the DCC key address (resource.dccAddress = dcc). The layout is:

  • A: [address, address + 256 KiB), four 64 KiB units, no DCC keys.
  • B: [address, address + 128 KiB), over A's first two units on purpose; that overlap is the hand-over under test.
  • B's DCC keys: surfaceB / 256 = 512 bytes at address + surfaceA, inside the 64 KiB after A.

The allocation, the guest registration and the host import are all surfaceA + 65536 = 320 KiB, so they cover A, B and the keys. readImage copies out of B's VkImage, not guest memory, and the final FlushPending(address, bytes) and readMemory(address, surfaceA) stay inside the block. I'll add a static_assert to the fixture so the overlap and the key placement are stated in the code rather than implied.

@MarcZX10

Copy link
Copy Markdown
Contributor

You are right. I misread the fourth surface() argument as the base address, but it sets dccAddress; both image resources start at address. The fixture intentionally overlaps B over A’s first two 64 KiB units, and the 320 KiB allocation covers A, B, and B’s key bytes. My bounds finding was incorrect; please disregard it. Thanks for correcting me.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6718 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L2059
PR 6820 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L2059
PR 8484 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L92-93, L146
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp L9281

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5426 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6146 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6185 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6501 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6636 docs/dev/ENVIRONMENT.md
PR 6658 docs/dev/ENVIRONMENT.md
PR 6682 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6683 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6868 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6946 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ENVIRONMENT.md
PR 6947 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ENVIRONMENT.md
PR 6951 docs/dev/ENVIRONMENT.md
PR 6960 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7002 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7085 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7446 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7449 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ENVIRONMENT.md
PR 7464 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 7484 docs/dev/ENVIRONMENT.md
PR 7505 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ENVIRONMENT.md
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/ENVIRONMENT.md
PR 7644 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7800 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7922 docs/dev/ENVIRONMENT.md
PR 8004 docs/dev/ENVIRONMENT.md
PR 8014 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8077 docs/dev/ENVIRONMENT.md
PR 8206 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 8267 core/libs/prx/libSceAgcDriver/CMakeLists.txt

@github-actions github-actions Bot added build agc libSceAgc* labels Oct 18, 2026
@parl0v
parl0v marked this pull request as draft October 18, 2026 06:54
@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Converting this to a draft. With this change I'm seeing a crash during Minecraft Dungeons II level loads (2 of 4 in-game loads on my RTX 4060: a game worker thread reads a garbage pointer, the same function both times), which I haven't seen on main in the same runs. I'm testing whether the commit I just pushed (23b8ddd, a CPU store into a handed-over unit before the receiving image writes now keeps the CPU's bytes, as on main) closes it, with the hand-over on and off in the same build. I'll mark it ready again only with clean repeated loads.

@parl0v
parl0v marked this pull request as ready for review October 18, 2026 07:14
@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review again. The crash isn't this change. It's Minecraft Dungeons II's world-load crash from late storage write-backs (#1970, the "Pending storage-image results" entry in docs/dev/debt/libSceAgcDriver.md), and it shows up on current main as well. Interleaved in-game loads on the RTX 4060 (APS5_TEXTURE_CACHE_MIB=3072), 4 each:

build crashes
main 66e71b6 1 / 4
this PR (23b8ddd) 2 / 4
this PR with APS5_NO_CLEAR_HAND_OVER=1 2 / 4

All three crash with the same two signatures: a loading thread or a background worker reads a garbage pointer during the level load. With the hand-over switched off the build does what main does, and it crashes as often as with it on. I'm checking separately whether this crash got more frequent on current main than in my earlier runs; that isn't specific to this PR.

The PR's numbers on current main (66e71b6), title screen:

main this PR
RTX 4060 8 GB, APS5_TEXTURE_CACHE_MIB=3072, 3 rounds 8.35 / 8.55 / 8.57 FPS 9.27 / 9.32 / 9.32 FPS +9.6 %, GPU busy 95 → 86 ms
RTX 3060 12 GB, default settings, 4 rounds 8.10 – 8.14 FPS 8.85 – 8.86 FPS +9.2 %, GPU busy 100 → 91 ms
Sword of the Sea (PPSA25344), RTX 3060, default settings, 2 rounds after a warm-up 10.09 / 10.10 FPS 10.40 / 10.39 FPS +3.0 %, GPU busy 78 → 76 ms

On the RTX 4060 the storage refresh write-backs go from 59–63 GB to 14.7 GB per 10 s (about 4,800 hand-overs per 10 s). The full build on 66e71b6 has no errors, and the 68 storage, DCC, depth and graphics tests pass.

@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Gameplay check on the RTX 4060 8 GB with the automatic cache: Minecraft Dungeons II loaded into a level, standing still for 90 s, FPS over the last 60 s. Five alternating rounds of main 66e71b6, this PR, and this PR with APS5_NO_CLEAR_HAND_OVER=1:

1 2 3 4 5
main 4.37 4.33 4.37 3.42 3.33
this PR 4.17 4.38 3.13 4.33 3.50
this PR, switch off 4.18 4.35 4.23 4.35 4.40

Storage refreshes are rare in gameplay (about 14 per 10 s, 10 MiB), and the hand-over fired 0 times in every PR row, so the PR doesn't change anything there. The 3.1 to 3.5 rows show up on main too: GPU busy stays the same and the idle gaps grow from 150 ms to 210–236 ms per frame. The gain is on the title screen, where clear-key write-backs make up 90 % of the storage-refresh bytes.

@oneandonlydean oneandonlydean self-assigned this Oct 18, 2026
@github-actions github-actions Bot added perf Performance change perf numbers missing perf pull request without a before and after summary line labels Oct 18, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

The on/off byte comparison in agc_storage_clear_hand_over is a good test when it runs. But clearHandOverTests returns normally on every path where it can't run: no host imports, imports compared on the device, no write watch, import refused, or the block leaving the watch. --clear-hand-over-only then returns 0, so the ctest shows Passed even when the watched case, the only one that hands over, never ran. SKIP_RETURN_CODE 77 is set but never reached.

Could those paths return a "not tested" result that makes the binary exit 77? At least the watched run's should, since the compared run alone only checks that no hand-over happens. Then a Passed result on CI means the hand-over was exercised.

@parl0v
parl0v force-pushed the perf/clear-keys-hand-over branch from 23b8ddd to 9371b6b Compare October 18, 2026 20:19
@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Done in 1d8072a (now on top of the rebase onto current main): clearHandOverTests returns whether it ran, and --clear-hand-over-only exits 77 when the watched run can't run (no host imports, imports compared on the device, no write watch, the import refused, or the block leaving the watch), so CI reports it as skipped instead of passed. The compared run still runs first and checks that nothing is handed over there. On my RTX 4060 (Windows) the watched run runs and the test passes.

The rebase also needed a row for APS5_NO_CLEAR_HAND_OVER in docs/dev/ENVIRONMENT.md, which check_conventions now asks for.

@github-actions

Copy link
Copy Markdown

This pull request is labeled perf, so it needs the summary line of a capture on main and one with the change, from the same title, scene and machine, as the Performance report form asks. Please paste both aps5-summary.jsonl lines, attach the two traces and add the output of tools/trace_digest.py.

@github-actions github-actions Bot added docs and removed perf numbers missing perf pull request without a before and after summary line labels Oct 18, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for the exit-77 change, it closes the false pass. One more line is needed so the Linux job accepts the skip. tools/check_skips.py fails the Linux job for any skipped test whose output has no skipped, <device limitation> line matching its DEVICE_LIMITATION pattern. --clear-hand-over-only now exits 77 but prints only "host imports are compared on this device: clear-key hand-over (watched) not tested", which doesn't match. On lavapipe, where host-pointer imports are compared rather than watched, the job would then fail at check_skips.

--shadowed-key-stores-only handles the same condition this way:

std::cout << "skipped, the device imports no watched host memory for the clear-key hand-over\n";
return 77;

Could --clear-hand-over-only print a line like that before returning 77? "the device imports no" is in the pattern.

@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Added in a5dcb10: when the watched run can't run, --clear-hand-over-only now prints "skipped, the device imports no watched host memory for the clear-key hand-over" before returning 77, the same wording as --shadowed-key-stores-only, and it matches DEVICE_LIMITATION. On my RTX 4060 the watched run still runs and the test passes. The performance capture in the description is on 9371b6b; this commit only touches the test.

@parl0v

parl0v commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Traces of the performance capture in the description (they open in ui.perfetto.dev):

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
… clear code

A storage image refreshed over memory where other images still had results
pending stored those results first, then uploaded. When the image's DCC keys
are a clear code its upload is a GPU clear that reads none of that memory, so
those stores were dead for it. Unreal's render graph does this whenever it
hands transient memory to a new texture it fast-clears.

In write-watched memory the results now stay pending. Anything that reads the
memory before the image is written stores them as before. Once the image's own
results cover their 64 KiB units they are dropped, as its write-back stores
every byte of those units; an image written again since keeps its newer
results, and a unit stored after the hand-over no longer counts as a CPU store
against the image's write. The path is chosen from the keys read before any
flush, and only when no flush is needed. APS5_NO_CLEAR_HAND_OVER=1 stores the
results first as before.
@parl0v
parl0v force-pushed the perf/clear-keys-hand-over branch from a5dcb10 to 278ea58 Compare October 19, 2026 06:19
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 19, 2026
# Conflicts:
#	core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants